import { Construct } from 'constructs';
import { CfnJobDefinitionProps } from './batch.generated';
import { Duration, IResource, Lazy, Resource } from '../../core';

/**
 * Represents a JobDefinition
 */
export interface IJobDefinition extends IResource {
  /**
   * The ARN of this job definition
   *
   * @attribute
   */
  readonly jobDefinitionArn: string;

  /**
   * The name of this job definition
   *
   * @attribute
   */
  readonly jobDefinitionName: string;

  /**
   * The default parameters passed to the container
   * These parameters can be referenced in the `command` that
   * you give to the container
   *
   * @see https://docs.aws.amazon.com/batch/latest/userguide/job_definition_parameters.html#parameters
   *
   * @default none
   */
  readonly parameters?: { [key:string]: any };

  /**
   * The number of times to retry a job.
   * The job is retried on failure the same number of attempts as the value.
   *
   * @default 1
   */
  readonly retryAttempts?: number;

  /**
   * Defines the retry behavior for this job
   *
   * @default - no `RetryStrategy`
   */
  readonly retryStrategies: RetryStrategy[];

  /**
   * The priority of this Job. Only used in Fairshare Scheduling
   * to decide which job to run first when there are multiple jobs
   * with the same share identifier.
   *
   * @default none
   */
  readonly schedulingPriority?: number;

  /**
   * The timeout time for jobs that are submitted with this job definition.
   * After the amount of time you specify passes,
   * Batch terminates your jobs if they aren't finished.
   *
   * @default - no timeout
   */
  readonly timeout?: Duration;

  /**
   * Add a RetryStrategy to this JobDefinition
   */
  addRetryStrategy(strategy: RetryStrategy): void;
}

/**
 * Props common to all JobDefinitions
 */
export interface JobDefinitionProps {
  /**
   * The name of this job definition
   *
   * @default - generated by CloudFormation
   */
  readonly jobDefinitionName?: string;

  /**
   * The default parameters passed to the container
   * These parameters can be referenced in the `command` that
   * you give to the container
   *
   * @see https://docs.aws.amazon.com/batch/latest/userguide/job_definition_parameters.html#parameters
   *
   * @default none
   */
  readonly parameters?: { [key:string]: any };

  /**
   * The number of times to retry a job.
   * The job is retried on failure the same number of attempts as the value.
   *
   * @default 1
   */
  readonly retryAttempts?: number;

  /**
   * Defines the retry behavior for this job
   *
   * @default - no `RetryStrategy`
   */
  readonly retryStrategies?: RetryStrategy[];

  /**
   * The priority of this Job. Only used in Fairshare Scheduling
   * to decide which job to run first when there are multiple jobs
   * with the same share identifier.
   *
   * @default none
   */
  readonly schedulingPriority?: number;

  /**
   * The timeout time for jobs that are submitted with this job definition.
   * After the amount of time you specify passes,
   * Batch terminates your jobs if they aren't finished.
   *
   * @default - no timeout
   */
  readonly timeout?: Duration;
}

/**
 * Define how Jobs using this JobDefinition respond to different exit conditions
 */
export class RetryStrategy {
  /**
   * Create a new RetryStrategy
   */
  public static of(action: Action, on: Reason) {
    return new RetryStrategy(action, on);
  }

  /**
   * The action to take when the job exits with the Reason specified
   */
  public readonly action: Action;

  /**
   * If the job exits with this Reason it will trigger the specified Action
   */
  public readonly on: Reason;

  constructor(action: Action, on: Reason) {
    this.action = action;
    this.on = on;
  }
}

/**
 * The Action to take when all specified conditions in a RetryStrategy are met
 */
export enum Action {
  /**
   * The job will not retry
   */
  EXIT = 'EXIT',
  /**
   * The job will retry. It can be retried up to the number of times specified in `retryAttempts`.
   */
  RETRY = 'RETRY',
}

/**
 * The corresponding Action will only be taken if *all* of the conditions specified here are met.
 */
export interface CustomReason {
  /**
   * A glob string that will match on the job exit code. For example, `'40*'` will match 400, 404, 40123456789012
   *
   * @default - will not match on the exit code
   */
  readonly onExitCode?: string;

  /**
   * A glob string that will match on the statusReason returned by the exiting job.
   * For example, `'Host EC2*'` indicates that the spot instance has been reclaimed.
   *
   * @default - will not match on the status reason
   */
  readonly onStatusReason?: string;

  /**
   * A glob string that will match on the reason returned by the exiting job
   * For example, `'CannotPullContainerError*'` indicates that container needed to start the job could not be pulled.
   *
   * @default - will not match on the reason
   */
  readonly onReason?: string;
}

/**
 * Common job exit reasons
 */
export class Reason {
  /**
   * Will match any non-zero exit code
   */
  static readonly NON_ZERO_EXIT_CODE: Reason = {
    onExitCode: '*',
  };

  /**
   * Will only match if the Docker container could not be pulled
   */
  static readonly CANNOT_PULL_CONTAINER: Reason = {
    onReason: 'CannotPullContainerError:*',
  }

  /**
   * Will only match if the Spot instance executing the job was reclaimed
   */
  static readonly SPOT_INSTANCE_RECLAIMED: Reason = {
    onStatusReason: 'Host EC2*',
  }

  /**
   * A custom Reason that can match on multiple conditions.
   * Note that all specified conditions must be met for this reason to match.
   */
  static custom(customReasonProps: CustomReason): Reason {
    return customReasonProps;
  }
}

/**
 * Abstract base class for JobDefinitions
 *
 * @internal
 */
export abstract class JobDefinitionBase extends Resource implements IJobDefinition {
  public readonly abstract jobDefinitionArn: string;
  public readonly abstract jobDefinitionName: string;

  public readonly parameters?: { [key:string]: any };
  public readonly retryAttempts?: number;
  public readonly retryStrategies: RetryStrategy[];
  public readonly schedulingPriority?: number;
  public readonly timeout?: Duration;

  constructor(scope: Construct, id: string, props?: JobDefinitionProps) {
    super(scope, id, {
      physicalName: props?.jobDefinitionName,
    });

    this.parameters = props?.parameters;
    this.retryAttempts = props?.retryAttempts;
    this.retryStrategies = props?.retryStrategies ?? [];
    this.schedulingPriority = props?.schedulingPriority;
    this.timeout = props?.timeout;
  }

  addRetryStrategy(strategy: RetryStrategy): void {
    this.retryStrategies.push(strategy);
  }
}

/**
 * @internal
 */
export function baseJobDefinitionProperties(baseJobDefinition: JobDefinitionBase): CfnJobDefinitionProps {
  return {
    parameters: baseJobDefinition.parameters,
    retryStrategy: {
      attempts: baseJobDefinition.retryAttempts,
      evaluateOnExit: Lazy.any({
        produce: () => {
          if (baseJobDefinition.retryStrategies.length === 0) {
            return undefined;
          }
          return baseJobDefinition.retryStrategies.map((strategy) => {
            return {
              action: strategy.action,
              ...strategy.on,
            };
          });
        },
      }),
    },
    schedulingPriority: baseJobDefinition.schedulingPriority,
    timeout: {
      attemptDurationSeconds: baseJobDefinition.timeout?.toSeconds(),
    },
    type: 'dummy',
  };
}
